﻿#region Using Statements
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using DirectZZT.Shared.Core;
using DirectZZT.Shared.Scripting;
using DirectZZT.Shared.World.Objects;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
#endregion

namespace DirectZZT.Shared.World
{
    /// <summary>
    /// Represents a DirectZZT board.
    /// </summary>
    [Serializable()]
    public class Board : BaseObject, IScriptExecutor
    {
        #region Constants

        /// <summary>
        /// Defines the amount of board objects on the X axis.
        /// </summary>
        public const int BoardWidth = 60;
        /// <summary>
        /// Defines the amount of board objects on the Y axis.
        /// </summary>
        public const int BoardHeight = 25;
        /// <summary>
        /// Defines the time in milliseconds at which a timer step/tick is performed.
        /// </summary>
        private const int TimerStep = 250;

        private const string MissingAssetName = "Textures/PlayfieldObjects/_Missing";
        
        #endregion

        #region Fields

        private List<BoardObject> _objects;
        private int _currentId;
        private Thread _scriptsThread;
        private bool _scriptsThreadBreak;
        private bool _isInitialized;
        private Stopwatch _globalTickTimer;

        #endregion

        #region Properties

        /// <summary>
        /// Gets/sets the board's id.
        /// </summary>
        public int BoardId { get; set; }
        /// <summary>
        /// Gets/sets the board's title.
        /// </summary>
        public string Title { get; set; }
        /// <summary>
        /// Gets/sets the world to which we belong to.
        /// </summary>
        public World World { get; set; }
        /// <summary>
        /// Gets the game associated with this board.
        /// </summary>
        public Game Game { get; private set; }
        /// <summary>
        /// Gets whether or not this board is active.
        /// A board is active when it is currently "on screen".
        /// Only active boards have scripts, inputs etc. processed
        /// </summary>
        public bool IsActive { get; private set; }
        /// <summary>
        /// Gets/sets whether or not this board is dark.
        /// </summary>
        public bool IsDark { get; set; }
        /// <summary>
        /// This is the board number of the board that the player teleports to when it touches a Board Edge from below.
        /// </summary>
        public int BoardNorthIndex { get; set; }
        /// <summary>
        /// This is the board number of the board that the player teleports to when it touches a Board Edge from below.
        /// </summary>
        public int BoardEastIndex { get; set; }
        /// <summary>
        /// This is the board number of the board that the player teleports to when it touches a Board Edge from below.
        /// </summary>
        public int BoardWestIndex { get; set; }
        /// <summary>
        /// This is the board number of the board that the player teleports to when it touches a Board Edge from below.
        /// </summary>
        public int BoardSouthIndex { get; set; }
        /// <summary>
        /// Gets/sets whether the player re-enters the board when being zapped.
        /// </summary>
        public bool ReEnterWhenZapped { get; set; }
        /// <summary>
        /// Gets/sets the re-entering position of the player when being zapped.
        /// This only takes effect if <see cref="ReEnterWhenZapped"/> is set to <c>true</c>.
        /// </summary>
        public bool ReEnterPositionX { get; set; }
        /// <summary>
        /// Gets/sets the re-entering position of the player when being zapped.
        /// This only takes effect if <see cref="ReEnterWhenZapped"/> is set to <c>true</c>.
        /// </summary>
        public bool ReEnterPositionY { get; set; }
        /// <summary>
        /// Gets/sets the time limit for this board.
        /// </summary>
        public int TimeLimit { get; set; }

        #endregion

        #region Constructors

        /// <summary>
        /// Constructor
        /// </summary>
        public Board()
        {
            _objects = new List<BoardObject>();
            _globalTickTimer = new Stopwatch();
        }

        #endregion

        #region Methods

        /// <summary>
        /// Initializes the board.
        /// </summary>
        /// <param name="game">The game to use.</param>
        public void Initialize(Game game)
        {
            this.Game = game;

            // construct board edges
            // north
            for (int i = -1; i <= Board.BoardWidth; i++)
            {
                BoardEdgeBoardObject north = new BoardEdgeBoardObject();
                north.Side = Direction.North;
                north.X = i;
                north.Y = -1;
                this.SpawnObject(north, null);
            }
            // west
            for (int i = 0; i < Board.BoardHeight; i++)
            {
                BoardEdgeBoardObject west = new BoardEdgeBoardObject();
                west.Side = Direction.West;
                west.X = -1;
                west.Y = i;
                this.SpawnObject(west, null);
            }
            // east
            for (int i = 0; i < Board.BoardHeight; i++)
            {
                BoardEdgeBoardObject east = new BoardEdgeBoardObject();
                east.Side = Direction.East;
                east.X = Board.BoardWidth;
                east.Y = i;
                this.SpawnObject(east, null);
            }
            // south
            for (int i = -1; i <= Board.BoardWidth; i++)
            {
                BoardEdgeBoardObject south = new BoardEdgeBoardObject();
                south.Side = Direction.South;
                south.X = i;
                south.Y = Board.BoardHeight;
                this.SpawnObject(south, null);
            }

            _isInitialized = true;

            // start off timer
            _globalTickTimer = Stopwatch.StartNew();
        }

        /// <summary>
        /// Updates the board and all objects on it.
        /// </summary>
        /// <param name="gameTime"></param>
        public void Update(GameTime gameTime)
        {
            if (!_isInitialized)
            {
                return;
            }

            // check timer tick period, and send timer tick to each board object
            if (_globalTickTimer.ElapsedMilliseconds > TimerStep)
            {
                for (int i = 0; i < _objects.Count; i++)
                {
                    _objects[i].OnTimerTick();
                }
                _globalTickTimer.Restart();
            }

            for (int i = 0; i < _objects.Count; i++)
            {
                BoardObject po = _objects[i];
                if (po.Enabled)
                {
                    // first check for the bounds, if this one shall be inside
                    if (!po.Capabilities.HasFlag(BoardObjectCapabilities.AllowOutsideBounds) && ((po.X < 0 || po.X > BoardWidth)
                        || (po.Y < 0 || po.Y > BoardHeight)))
                    {
                        Logging.GetLogging().LogFormat(LoggingMessageType.Warning, "Board", "", "BoardObject '{0}' with name '{1}' is outside allowed bounds of this board (X: {2}, Y: {3}). It has been disabled.", po.Id, po.Name, po.X, po.Y);
                        po.Enabled = false;
                        po.Visible = false;
                    }
                    else
                    {
                        po.Update(gameTime);
                    }
                }
            }
        }

        /// <summary>
        /// Draws the board and all objects on it.
        /// </summary>
        /// <param name="gameTime"></param>
        public void Draw(GameTime gameTime)
        {
            for (int i = 0; i < _objects.Count; i++)
            {
                BoardObject obj = _objects[i];
                if (obj.Visible)
                {
                    if (DetermineVisibilityInDarkness(obj.X, obj.Y))
                    {
                        Texture2D texture = World.TextureManager.GetResource(obj.Texture);
                        if (texture == null)
                        {
                            // disable rendering
                            obj.Visible = false;
                            Logging.GetLogging().LogFormat(LoggingMessageType.Warning, "Board", "", "Couldn't load texture from asset '{0}'. This object's visuals have been disabled.", obj.Texture);
                            return;
                        }

                        // calculate target rectangle on the screen
                        Rectangle target = new Rectangle()
                        {
                            Height = 32,
                            Width = 32,
                            X = (obj.X + 1) * 32,
                            Y = (obj.Y + 1) * 32,
                        };
                        // third-last parameter means offsetting everything a bit so that rotation is done correctly
                        this.World.SpriteBatch.Draw(texture, target, null, obj.Color, obj.TextureRotation, new Vector2(16f, 16f), SpriteEffects.None, 0.0f);
                    }
                    else
                    {
                        // TODO: draw gradient color
                    }
                }
            }
        }

        private bool DetermineVisibilityInDarkness(int x, int y)
        {
            if (!IsDark)
            {
                return true;
            }
            // TODO: find out player positions
            return true;
        }

        /// <summary>
        /// Returns the board objects with the given name.
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public BoardObject[] Find(string name)
        {
            return _objects.FindAll(bo => bo.Name == name).ToArray();
        }

        /// <summary>
        /// Called by the world when the board becomes active (the player is on this board).
        /// </summary>
        public void OnEnter()
        {
            this.IsActive = true;
            StartScripts();
        }

        /// <summary>
        /// Called by the world when the player moves onto another board.
        /// </summary>
        public void OnLeave()
        {
            this.IsActive = false;
            StopScripts();
        }

        /// <summary>
        /// Called when the board is disposed.
        /// </summary>
        public override void Dispose()
        {
            base.Dispose();
            this.Stop();
        }

        /// <summary>
        /// Stops the board.
        /// </summary>
        public void Stop()
        {
            StopScripts();
        }

        private void StopScripts()
        {
            if (_scriptsThread == null)
            {
                // appearantly this board is already stopped
                return;
            }
            _scriptsThreadBreak = true;
            _scriptsThread.Join();
            _scriptsThread = null;
        }

        private void StartScripts()
        {
            if (_scriptsThread == null)
            {
                _scriptsThread = new Thread(ScriptsThread);
                _scriptsThread.Start();
            }
            else
            {
                Logging.GetLogging().Log(LoggingMessageType.Warning, "Board", "Scripts", "Attempt was made to start the scripts thread twice!");
            }
        }

        /// <summary>
        /// Returns the BoardObject at the given location, if any.
        /// </summary>
        /// <param name="x">The x coordinate.</param>
        /// <param name="y">The y coordinate.</param>
        /// <returns>The BoardObject at the given location, if any.
        /// -or- <c>null</c>, if there is no BoardObject at that location.</returns>
        public BoardObject GetObjectAt(int x, int y)
        {
            return _objects.FirstOrDefault(t => t.X == x && t.Y == y);
        }

        /// <summary>
        /// Returns an array containing all players on this board.
        /// </summary>
        /// <returns>An array containing all players on this board.</returns>
        public BoardObject[] GetPlayers()
        {
            return _objects.Where(bo => bo is IPlayerBoardObject).ToArray();
        }

        /// <summary>
        /// Spawns a new object on the board.
        /// </summary>
        /// <param name="obj">The object to spawn.</param>
        /// <param name="modifiers">Optional modifiers that were given to when we shall spawn (don't confuse with Object Parameters!).</param>
        /// <returns>Whether or not the object could be spawned.</returns>
        public bool SpawnObject(BoardObject obj, string[] modifiers)
        {
            lock (_objects)
            {
                // don't duplicately add objects there if there are any
                if (_objects.Any(o => o.X == obj.X && o.Y == obj.Y))
                {
                    Logging.GetLogging().LogFormat(LoggingMessageType.Warning, "Game", "", "Cannot add multiple objects at {0},{1}! Object wasn't spawned.", obj.X, obj.Y);
                    return false;
                }

                obj.Board = this;

                // add it to the objects list
                _objects.Add(obj);
                obj.OnAttach();

                // set the important stuff
                obj.Id = ++_currentId;

                // and lets go!
                obj.ScriptExecutor = this;
                obj.OnSpawn(modifiers);

                return true;
            }
        }

        /// <summary>
        /// Makes the object "dead" and removes it from the board.
        /// </summary>
        /// <param name="obj"></param>
        public void DieObject(BoardObject obj)
        {
            lock (_objects)
            {
                obj.OnDie();
                obj.Dispose();
                _objects.Remove(obj);
            }
        }

        /// <summary>
        /// Lets the <paramref name="source"/> object "become" another one. 
        /// </summary>
        /// <param name="source">The object that shall become another one.</param>
        /// <param name="destination">The object.</param>
        public void BecomeObject(BoardObject source, BoardObject destination)
        {
            // remove object from the board
            DieObject(source);

            // do that only if it is a non-EMPTY object
            if (destination != null)
            {
                destination.X = source.X;
                destination.Y = source.Y;
                destination.Name = source.Name;
                // TODO: what else to transfer?

                SpawnObject(destination, destination.Modifiers.ToArray());
                destination.OnBecome(source);
            }
        }

        #endregion

        #region Persistency methods

        /// <summary>
        /// Retrieves the serialization data for this board.
        /// </summary>
        /// <returns></returns>
        public BoardSerializationData GetSerializationData()
        {
            BoardSerializationData data = new BoardSerializationData();

            foreach (BoardObject obj in _objects)
            {
                BoardObjectAttribute attribute = BoardObjectAttribute.GetAttribute(obj.GetType());
                // if either there is no attribute or there is an attribute, but we shall not serialize that one
                if (attribute == null || !attribute.Serialize)
                {
                    // ignore this 
                    continue;
                }

                // yes, serialize this one
                data.Data.Add(new BoardObjectSerializationData()
                {
                    Id = attribute.Identifier,
                    Object = obj,
                });
            }


            return data;
        }

        #endregion

        #region Scripting

        /// <summary>
        /// Performs an infinite loop which processes each script in a separate thread.
        /// </summary>
        private void ScriptsThread()
        {
            while (!_scriptsThreadBreak)
            {
                lock (_objects)
                {
                    // update all our objects' scripts
                    for (int i = 0; i < _objects.Count; i++)
                    {
                        BoardObject bo = _objects[i];

                        // TODO: this is dangerous... an object (including its scripts) can die and then we might accidentally skip one, despite the lock!
                        for (int j = 0; j < bo.GetScripts().Count; j++)
                        {
                            IScript script = bo.GetScripts()[j];
                            // keep it running
                            if (script.State == ScriptState.Running
                                || script.State.HasFlag(ScriptState.DEBUG_STEP))
                            {
                                script.Update();
                            }
                        }
                    }
                }
                Thread.Sleep(1);
            }

            // break all scripts
            Logging.GetLogging().Log(LoggingMessageType.Debug, "Game", "", "Canceling scripts, please wait...");
            foreach (var item in _objects.SelectMany(b => b.GetScripts()))
            {
                item.Stop();
            }
            Logging.GetLogging().Log(LoggingMessageType.Debug, "Game", "", "Canceling scripts done!");
        }

        /// <summary>
        /// Sends an asynchronous, manual event to the specified <see cref="BoardObject"/>.
        /// </summary>
        /// <param name="destination">The <see cref="BoardObject"/> to send an event to.</param>
        /// <param name="eventName">The event name.</param>
        /// <param name="arguments">Optional arguments, appropriate for the event.</param>
        /// <returns>Whether or not the event could be sent.</returns>
        public bool SendEvent(BoardObject destination, string eventName, params string[] arguments)
        {
            if (destination == null)
            {
                throw new ArgumentNullException("destination");
            }

            // deliver the event
            destination.OnEvent(eventName, arguments);
            return true;
        }

        #endregion

        #region IScriptExecutor Members

        void IScriptExecutor.SendEvent(string objectName, string eventName, params string[] arguments)
        {
            BoardObject[] bo = Find(objectName);
            if (bo.Length > 0)
            {
                // send to all objects (since there can be multiple objects with the same name)
                for (int i = 0; i < bo.Length; i++)
                {
                    bo[i].OnEvent(eventName, arguments);
                }
            }
        }

        ScriptFlag IScriptExecutor.GetFlag(string name)
        {
            if (this.World.Data.Flags.ContainsKey(name))
            {
                return this.World.Data.Flags[name];
            }
            return null;
        }

        bool IScriptExecutor.SetFlag(string name, object value)
        {
            // check for featureset-compatibility
            // only allow adding if we haven't reached 10 flags
            if (!this.World.Data.Flags.ContainsKey(name)
                && (this.World.Data.Flags.Count == 10 && this.World.FeatureSet.IsFeatureSupported(Feature.MoreThanTenFlags)))
            {
                return false;
            }

            ScriptFlag flag = new ScriptFlag();
            flag.SetValue(value);

            this.World.Data.Flags[name] = flag;
            return true;
        }

        void IScriptExecutor.RemoveFlag(string name)
        {
            if (this.World.Data.Flags.ContainsKey(name))
            {
                this.World.Data.Flags.Remove(name);
            }
        }

        #endregion

    }
}
